#include "preHeader.h"
#include "TeleportMgr.h"
#include "CharacterMgr.h"
#include "Game/AccountMgr.h"
#include "Team/Team.h"
#include "Session/GateServerSession.h"
#include "Session/MapServerMgr.h"

TeleportMgr::TeleportMgr()
{
}

TeleportMgr::~TeleportMgr()
{
}

void TeleportMgr::Tick()
{
	for (auto itr = m_playerLoadInfos.begin(); itr != m_playerLoadInfos.end();) {
		auto&[playerGuid, plInfo] = *itr++;
		if (plInfo.expireTime < GET_UNIX_TIME) {
			sAccountMgr.KickAccount(plInfo.pChar->acct, LOGIN_ERROR_CHARACTER_TIMEOUT);
		}
	}
}

void TeleportMgr::HandleLoginRequest(ObjGUID playerGuid,
	ObjGUID instOwner, InstGUID instGuid, const vector3f1f& pos,
	TeleportType type)
{
	HandleTeleportRequest(
		playerGuid, instOwner, instGuid, pos, type, 0, emptyStringView);
}

void TeleportMgr::HandleTeleportRequest(ObjGUID playerGuid,
	ObjGUID instOwner, InstGUID instGuid, const vector3f1f& pos,
	TeleportType type, uint32 flags, const std::string_view& args)
{
	if (HasTeleportInfo(playerGuid) || HasPlayerLoadInfo(playerGuid)) {
		return;
	}

	Character* pChar = sCharacterMgr.GetCharacter(playerGuid);
	if (pChar == NULL) {
		return;
	}

	const MapInfo* pToMapInfo = GetDBEntry<MapInfo>(instGuid.MAPID);
	if (pToMapInfo == NULL) {
		return;
	}

	ExtTeleportInfo tpInfo;
	tpInfo.pChar = pChar;
	tpInfo.playerGuid = playerGuid.objGUID;
	tpInfo.ownerGuid = instOwner.objGUID;
	tpInfo.instGuid = instGuid.instGUID;
	tpInfo.x = pos.x;
	tpInfo.y = pos.y;
	tpInfo.z = pos.z;
	tpInfo.o = pos.o;
	tpInfo.type = (u32)type;
	tpInfo.flags = flags;
	tpInfo.args = args;

	tpInfo.ownerGuid = GetInstanceOwner(tpInfo).objGUID;
	tpInfo.instGuid = GetInstanceGuid(tpInfo).instGUID;

	m_teleportInfos.emplace(playerGuid, tpInfo);
	BeginEnterInstance(tpInfo);
}

void TeleportMgr::BeginEnterInstance(const ExtTeleportInfo& tpInfo)
{
	if (!tpInfo.pChar->isOnline) {
		HandleBeginEnterInstanceRespon(CharOffline, tpInfo.pChar->guid);
		return;
	}

	MapServerProxy* pTargetMS =
		sMapServerMgr.GetMapServer(GetInstGuidFromValue(tpInfo.instGuid));
	if (pTargetMS == NULL) {
		HandleBeginEnterInstanceRespon(
			LOGIN_ERROR_MAP_SERVER_OFFLINE, tpInfo.pChar->guid);
		return;
	}

	NetPacket packToMS(GS_CHARACTER_TELEPORT_BEGIN_ENTER_INSTANCE);
	SaveToINetStream((const CharTeleportInfo&)tpInfo, packToMS);
	pTargetMS->SendPacket(packToMS);
}

void TeleportMgr::HandleBeginEnterInstanceRespon(
	GErrorCode errCode, ObjGUID playerGuid,
	ObjGUID instOwner, InstGUID instGuid, const vector3f1f& pos)
{
	auto tpInfo = GetTeleportInfo(playerGuid);
	if (tpInfo == NULL) {
		return;
	}

	if (errCode == CommonSuccess) {
		tpInfo->ownerGuid = instOwner.objGUID;
		tpInfo->instGuid = instGuid.instGUID;
		tpInfo->x = pos.x;
		tpInfo->y = pos.y;
		tpInfo->z = pos.z;
		tpInfo->o = pos.o;
		TryInitNewInstance(*tpInfo);
		switch (TeleportType(tpInfo->type)) {
		case TeleportType::Login:
		case TeleportType::Reconnect:
			LoadMapToEnterTargetMS(playerGuid);
			break;
		default:
			if (!LeaveFromSourceMS(playerGuid)) {
				TeleportRequestFailed(playerGuid, CommonNoWarning);
			}
			break;
		}
	} else {
		switch (TeleportType(tpInfo->type)) {
		case TeleportType::Login:
		case TeleportType::Reconnect:
			ReformTeleportRequest(*tpInfo);
			break;
		default:
			TeleportRequestFailed(playerGuid, errCode);
			break;
		}
	}
}

void TeleportMgr::ReformTeleportRequest(const ExtTeleportInfo& tpInfo)
{
	auto instOwner = GetGuidFromValue(tpInfo.ownerGuid);
	auto instGuid = GetInstGuidFromValue(tpInfo.instGuid);
	auto pChar = tpInfo.pChar;
	auto type = tpInfo.type;
	auto flags = tpInfo.flags;
	auto args = tpInfo.args;
	RemoveTeleportInfo(pChar->guid);

	auto pMapInfo = GetDBEntry<MapInfo>(instGuid.MAPID);
	if (pMapInfo == NULL) {
		return;
	}
	if (pMapInfo->pop_map_id == pMapInfo->Id) {
		return;
	}

	HandleTeleportRequest(pChar->guid, instOwner,
		MakeInstGuid((u16)MapInfo::Type::WorldMap, pMapInfo->pop_map_id),
		{pMapInfo->pop_pos_x, pMapInfo->pop_pos_y, pMapInfo->pop_pos_z,
		 pMapInfo->pop_o},
		(TeleportType)type, flags, args);
}

void TeleportMgr::TryInitNewInstance(const ExtTeleportInfo& tpInfo)
{
	if (tpInfo.args.empty()) {
		return;
	}

	NetPacket packToMI(G2M_INITIALIZE_INSTANCE);
	packToMI.Append(tpInfo.args.data(), tpInfo.args.size());
	sMapServerMgr.RouteToInstance(GetInstGuidFromValue(tpInfo.instGuid), packToMI);
}

void TeleportMgr::HandleLeaveMapRespon(bool isSucc,
	ObjGUID playerGuid, InstGUID instGuid, const vector3f1f& pos)
{
	if (!isSucc) {
		TeleportRequestFailed(playerGuid, CommonNoWarning);
		return;
	}

	Character* pChar = sCharacterMgr.GetCharacter(playerGuid);
	if (pChar == NULL) {
		TeleportRequestFailed(playerGuid, CharNotExist);
		return;
	}

	if (instGuid != InstGUID_NULL) {
		if (HasTeleportInfo(playerGuid)) {
			SendToClientToLeaveMap(playerGuid);
			LoadMapToEnterTargetMS(playerGuid);
		}
	} else {
		if (!pChar->IsAccountOnline()) {
			SendToClientToLogoutGame(playerGuid);
		}
	}
}

bool TeleportMgr::LeaveFromSourceMS(ObjGUID playerGuid)
{
	auto tpInfo = GetTeleportInfo(playerGuid);
	if (tpInfo == NULL) {
		return false;
	}

	Character* pChar = tpInfo->pChar;
	auto pMapServerOld = sMapServerMgr.GetMapServer(pChar->lastInstGuid);
	if (pMapServerOld == NULL) {
		return false;
	}

	tpInfo->isOpLeave = true;
	pMapServerOld->RemoveCharacter(pChar);

	return true;
}

void TeleportMgr::LoadMapToEnterTargetMS(ObjGUID playerGuid)
{
	auto tpInfo = GetTeleportInfo(playerGuid);
	if (tpInfo == NULL) {
		return;
	}

	Character* pChar = tpInfo->pChar;
	pChar->lastInstOwner = GetGuidFromValue(tpInfo->ownerGuid);
	pChar->lastInstGuid = GetInstGuidFromValue(tpInfo->instGuid);
	pChar->lastPos = {tpInfo->x, tpInfo->y, tpInfo->z, tpInfo->o};

	NetPacket packToClient(SMSG_PLAYER_LOAD_MAP);
	packToClient << pChar->guid << pChar->lastInstGuid << pChar->lastPos;
	pChar->SendPacket(packToClient);

	ExtPlayerLoadInfo plInfo;
	plInfo.pChar = pChar;
	plInfo.playerGuid = tpInfo->playerGuid;
	plInfo.type = tpInfo->type;
	plInfo.flags = tpInfo->flags;
	plInfo.expireTime = GET_UNIX_TIME + 60;

	switch (TeleportType(tpInfo->type)) {
	case TeleportType::Login:
	case TeleportType::Reconnect:
		plInfo.isSaved = true;
		break;
	}

	m_playerLoadInfos.emplace(playerGuid, plInfo);
	RemoveTeleportInfo(playerGuid);
}

void TeleportMgr::SendToClientToLeaveMap(ObjGUID playerGuid)
{
	auto tpInfo = GetTeleportInfo(playerGuid);
	if (tpInfo == NULL) {
		return;
	}

	Character* pChar = tpInfo->pChar;
	if (pChar == NULL) {
		return;
	}

	NetPacket packToClient(SMSG_PLAYER_LEAVE_MAP);
	packToClient << playerGuid << pChar->lastInstGuid;
	pChar->SendPacket(packToClient);
}

void TeleportMgr::SendToClientToLogoutGame(ObjGUID playerGuid)
{
	Character* pChar = sCharacterMgr.GetCharacter(playerGuid);
	if (pChar == NULL) {
		return;
	}

	NetPacket packToClient(SMSG_CHARACTER_LOGOUT_RESP);
	packToClient << playerGuid;
	pChar->SendPacket(packToClient);
}

void TeleportMgr::TeleportRequestFailed(ObjGUID playerGuid, GErrorCode errCode)
{
	auto tpInfo = GetTeleportInfo(playerGuid);
	if (tpInfo == NULL) {
		return;
	}

	MapServerProxy* pMapServerNew =
		sMapServerMgr.GetMapServer(GetInstGuidFromValue(tpInfo->instGuid));
	if (pMapServerNew != NULL) {
		InstGUID instGuid = GetInstGuidFromValue(tpInfo->instGuid);
		NetPacket packCancel(G2M_CHARACTER_TELEPORT_BEGIN_ENTER_INSTANCE_CANCEL);
		packCancel << playerGuid;
		pMapServerNew->TransInstancePacket(instGuid, packCancel);
	}

	Character* pChar = tpInfo->pChar;
	auto pMapServerOld = sMapServerMgr.GetMapServer(pChar->lastInstGuid);
	if (pMapServerOld != NULL) {
		NetPacket packFailed(G2M_CHARACTER_TELEPORT_FAILED);
		packFailed << errCode;
		pMapServerOld->TransPlayerPacket(playerGuid, packFailed);
	}

	RemoveTeleportInfo(playerGuid);
}

void TeleportMgr::PlayerLoadRequestFailed(ObjGUID playerGuid, GErrorCode errCode)
{
	const auto pPLInfo = GetPlayerLoadInfo(playerGuid);
	if (pPLInfo == NULL) {
		return;
	}

	Character* pChar = pPLInfo->pChar;
	auto pMapServer = sMapServerMgr.GetMapServer(pChar->lastInstGuid);
	if (pMapServer != NULL) {
		NetPacket packCancel(G2M_CHARACTER_TELEPORT_BEGIN_ENTER_INSTANCE_CANCEL);
		packCancel << playerGuid;
		pMapServer->TransInstancePacket(pChar->lastInstGuid, packCancel);
	}

	RemovePlayerLoadInfo(playerGuid);
}

void TeleportMgr::OnEnterToTargetMS(MapServerProxy* pTargetServer, Character* pChar)
{
	PlayerTeleportInfo tpInfo;
	tpInfo.playerGuid = pChar->guid.objGUID;
	tpInfo.x = pChar->lastPos.x;
	tpInfo.y = pChar->lastPos.y;
	tpInfo.z = pChar->lastPos.z;
	tpInfo.o = pChar->lastPos.o;

	if (pChar->pTeam != NULL) {
		tpInfo.teamId = pChar->pTeam->getTeamId();
	}

	const auto pPLInfo = GetPlayerLoadInfo(pChar->guid);
	if (pPLInfo != NULL) {
		tpInfo.type = pPLInfo->type;
		tpInfo.flags = pPLInfo->flags;
	}

	auto pAccount = pChar->GetAccount();
	if (pAccount != NULL) {
		auto pSession = pAccount->GetSession();
		tpInfo.clientSN = pAccount->sn;
		tpInfo.gateSN = pSession->sn();
	}

	NetPacket packToMI(G2M_CHARACTER_ENTER_MAP);
	SaveToINetStream(tpInfo, packToMI);
	pTargetServer->TransInstancePacket(pChar->lastInstGuid, packToMI);

	RemovePlayerLoadInfo(pChar->guid);
}

void TeleportMgr::OnLeaveFromSourceMS(MapServerProxy* pSourceServer, Character* pChar)
{
	auto tpInfo = GetTeleportInfo(pChar->guid);
	if (tpInfo != NULL && tpInfo->isOpLeave) {
		tpInfo->isOpLeave = false;
		NetPacket packToPlayer(G2M_CHARACTER_LEAVE_MAP);
		packToPlayer << tpInfo->instGuid
			<< tpInfo->x << tpInfo->y << tpInfo->z << tpInfo->o;
		pSourceServer->TransPlayerPacket(pChar->guid, packToPlayer);
	} else {
		NetPacket packToMI(G2M_CHARACTER_LOGOUT_GAME);
		packToMI << pChar->guid;
		pSourceServer->TransInstancePacket(pChar->lastInstGuid, packToMI);
	}
}

void TeleportMgr::HandlePlayerLoadMapFinish(ObjGUID playerGuid)
{
	auto pPLInfo = GetPlayerLoadInfo(playerGuid);
	if (pPLInfo != NULL) {
		pPLInfo->isLoaded = true;
		TryEnterNewInstance(*pPLInfo);
	}
}

void TeleportMgr::HandleCharacterSaved(ObjGUID playerGuid)
{
	auto pPLInfo = GetPlayerLoadInfo(playerGuid);
	if (pPLInfo != NULL) {
		pPLInfo->isSaved = true;
		TryEnterNewInstance(*pPLInfo);
	}
}

void TeleportMgr::TryEnterNewInstance(const ExtPlayerLoadInfo& plInfo)
{
	if (!plInfo.isLoaded || !plInfo.isSaved) {
		return;
	}

	Character* pChar = plInfo.pChar;
	auto pMapServer = sMapServerMgr.GetMapServer(pChar->lastInstGuid);
	if (pMapServer != NULL) {
		pMapServer->AddCharacter(pChar);
	}
}

bool TeleportMgr::HasTeleportInfo(ObjGUID playerGuid) const
{
	return m_teleportInfos.count(playerGuid) != 0;
}

void TeleportMgr::RemoveTeleportInfo(ObjGUID playerGuid)
{
	m_teleportInfos.erase(playerGuid);
}

bool TeleportMgr::HasPlayerLoadInfo(ObjGUID playerGuid) const
{
	return m_playerLoadInfos.count(playerGuid) != 0;
}

void TeleportMgr::RemovePlayerLoadInfo(ObjGUID playerGuid)
{
	m_playerLoadInfos.erase(playerGuid);
}

TeleportMgr::ExtTeleportInfo* TeleportMgr::GetTeleportInfo(ObjGUID playerGuid)
{
	auto itr = m_teleportInfos.find(playerGuid);
	return itr != m_teleportInfos.end() ? &itr->second : NULL;
}

TeleportMgr::ExtPlayerLoadInfo* TeleportMgr::GetPlayerLoadInfo(ObjGUID playerGuid)
{
	auto itr = m_playerLoadInfos.find(playerGuid);
	return itr != m_playerLoadInfos.end() ? &itr->second : NULL;
}

ObjGUID TeleportMgr::GetInstanceOwner(const ExtTeleportInfo& tpInfo) const
{
	return GetGuidFromValue(tpInfo.ownerGuid);
}

InstGUID TeleportMgr::GetInstanceGuid(const ExtTeleportInfo& tpInfo) const
{
	return GetInstGuidFromValue(tpInfo.instGuid);
}

void TeleportMgr::DisposeMapServerOffline(MapServerProxy* pMapServer)
{
	for (auto itr = m_teleportInfos.begin(); itr != m_teleportInfos.end();) {
		auto&[playerGuid, tpInfo] = *itr++;
		MapServerProxy* pMapServerNew =
			sMapServerMgr.GetMapServer(GetInstGuidFromValue(tpInfo.instGuid));
		if (pMapServerNew == pMapServer) {
			sAccountMgr.KickAccount(tpInfo.pChar->acct, LOGIN_ERROR_MAP_SERVER_OFFLINE);
			continue;
		}
		auto pMapServerOld = sMapServerMgr.GetMapServer(tpInfo.pChar->lastInstGuid);
		if (pMapServerOld == pMapServer) {
			sAccountMgr.KickAccount(tpInfo.pChar->acct, LOGIN_ERROR_MAP_SERVER_OFFLINE);
			continue;
		}
	}
	for (auto itr = m_playerLoadInfos.begin(); itr != m_playerLoadInfos.end();) {
		auto&[playerGuid, plInfo] = *itr++;
		auto pMapServerNew = sMapServerMgr.GetMapServer(plInfo.pChar->lastInstGuid);
		if (pMapServerNew == pMapServer) {
			sAccountMgr.KickAccount(plInfo.pChar->acct, LOGIN_ERROR_MAP_SERVER_OFFLINE);
			continue;
		}
	}
}
